﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Input;
using System.Collections.Generic;
using AWLibrary;

namespace TestGame.GameLib {
    /// <summary>
    /// Represents a Player in the game. Handles input, drawing, and update 
    /// functions for the Player. Each unique player should have its own 
    /// PlayingArea. 
    /// </summary>
    class PlayingArea {
        #region Fields
        
        /// <summary>
        /// ActiveCombo are the two blobs that the player controls and places 
        /// in the blobs.
        /// </summary>
        BlobCombo activeCombo;

        /// <summary>
        /// NextCombo is the next BlobCombo to be used.
        /// </summary>
        BlobCombo nextCombo;

        /// <summary>
        /// The collection of all placed Blobs in the game. They are primarily 
        /// added to from ActiveBlob.
        /// </summary>
        BlobArray blobs;

        /// <summary>
        /// The Timer used inbetween when the Grid is ready for the next 
        /// BlobCombo and before it actually spawns.
        /// </summary>
        TimeSpan blobDelayTimer;

        /// <summary>
        /// Random number generator that is used to determine which Blobs spawn
        /// next.
        /// </summary>
        Random rng;

        /// <summary>
        /// A seed used to generate numbers in the random number generator.
        /// Initially it is not used, but when it is set, the random number
        /// generator is re-constructed using the seed.
        /// </summary>
        int seed;
        
        /// <summary>
        /// The current score of the player.
        /// </summary>
        int score;

        /// <summary>
        /// The amount of gray blobs to be added once the Grid is resolved.
        /// </summary>
        int grayBlobBuffer;

        /// <summary>
        /// A stack of "removed counts" that is used to determine the score
        /// bonus.
        /// </summary>
        Stack<int> scoreStack;

        /// <summary>
        /// These two variables are used to determine where in the Grid the 
        /// initial BlobCombo spawns.
        /// </summary>
        public const int INITIAL_BLOB_X = 4;
        public const int INITIAL_BLOB_Y = 9;

        #endregion

        #region Properties

        /// <summary>
        /// The graphical offset from (0,0) the blobs is drawn at.
        /// </summary>
        public static Vector2 GraphicOffset = new Vector2(15, 15);

        /// <summary>
        /// Determines if DebugMode is enabled. If debugmode is enabled, the game
        /// will render additional items to help with debugging.
        /// </summary>
        public static bool DebugMode = false;

        /// <summary>
        /// The current Score of the Player
        /// </summary>
        public int Score {
            get { return score; }
        }

        /// <summary>
        /// The seed of the Random Number Generator that determines which Blobs
        /// spawn next.
        /// </summary>
        public int Seed {
            get { return seed; }
            set { seed = value; rng = new Random(seed); }
        }

        #endregion


        #region Initialization

        /// <summary>
        /// Creates a new PlayingArea using the GameRules dictated size.
        /// </summary>
        public PlayingArea() {
            blobs = new BlobArray(GameRules.GridWidth, GameRules.GridHeight);
            
            scoreStack = new Stack<int>();
            blobDelayTimer = TimeSpan.Zero;
            rng = new Random();
            grayBlobBuffer = 0;
            score = 0;

            SetNextBlobCombo();
        }

        /// <summary>
        /// Loads the content needed, as well as any classes the PlayingArea 
        /// needs.
        /// </summary>
        /// <param name="content"></param>
        public static void LoadContent(ContentManager content) {
            BlobCombo.LoadContent(content);
            BlobArray.LoadContent(content);
            Blob.LoadContent(content);
            ParticleSystem.LoadContent(content);
        }

        #endregion

        #region Input Methods

        /// <summary>
        /// Handles the input for the PlayingArea. In most cases, most of the
        /// input handling is redirected to the BlobCombo class.
        /// </summary>
        /// <param name="input"></param>
        public void HandleInput(InputState input) {
            if (activeCombo != null) {
                activeCombo.HandleInput(input, blobs);
            }

            if (input.IsNewKeyPress(Keys.D)) {
                DebugMode = !DebugMode;
            }

            if (input.IsNewKeyPress(Keys.L)) {
                this.AddGrayBlobs(5);
            }
        }

        #endregion

        #region Update Methods

        /// <summary>
        /// Updates the PlayingArea, and will attempt to resolve the BlobArray
        /// if needs be.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime) {
            /* We are checking to see if there are any blobs that are waiting to be removed.
             * If there are, we don't bother attempting to resolve until they're done. */
            if (!blobs.AnyRemovableBlobs() && !blobs.IsResolved) {
                int count = blobs.Resolve();

                if (count > 0) {
                    scoreStack.Push(count);
                }
            }
            else if (blobs.IsResolved) {
                score += CalculateScoreStack();

                UpdateBlobCombos(gameTime);
            }

            ParticleSystem.Update(gameTime);
            blobs.Update(gameTime);
        }
        
        /// <summary>
        /// Updates the BlobCombo and decides when to spawn the next Blob Combo.
        /// </summary>
        /// <param name="gameTime"></param>
        private void UpdateBlobCombos(GameTime gameTime) {
            // If there is an active BlobCombo, call Update on it to move it down. 
            if (activeCombo != null) {
                activeCombo.Update(gameTime, blobs);

                /* The blobCombo is done when it has collided with the Grid/Blobs for a certain
                 * amount of time. Upon it finishing, we add the blobs to Grid and set the 
                 * BlobCombo to null, so it begins the Timer to spawn the next blobs. */
                if (activeCombo.IsFinished()) {
                    blobs.AddToPlayingArea(activeCombo.LeadBlob);
                    blobs.AddToPlayingArea(activeCombo.FollowBlob);
                    activeCombo = null;
                }
            }
            else if (ReadyForNextCombo(gameTime)) {
                /* If we don't have an active blob combo, we check if:
                 *      A. There are no Gray Blobs waiting to be added
                 *      B. The timer has run out
                 * If both are satisified, we then spawn the next combo. */
                if (grayBlobBuffer > 0) {
                    AddGrayBlobsToGrid();
                }
                else {
                    SetNextBlobCombo();
                }
            }
        }

        #endregion

        #region BlobCombo Methods

        /// <summary>
        /// Sets the next BlobCombo by updating the current to the next
        /// BlobCombo and by generating a new next combo.
        /// </summary>
        private void SetNextBlobCombo() {
            if (nextCombo != null) {
                activeCombo = nextCombo;
                nextCombo = new BlobCombo(rng);
            }
            else {
                activeCombo = new BlobCombo(rng);
                nextCombo = new BlobCombo(rng);
            }
        }

        /// <summary>
        /// Determines if the PlayingArea is ready for the next combo.
        /// </summary>
        /// <param name="gameTime"></param>
        /// <returns>True if the PlayingArea is ready, false otherwise.</returns>
        private bool ReadyForNextCombo(GameTime gameTime) {
            bool isReady = false;

            // If the time hasn't started, start it timer for 1 second
            if (blobDelayTimer.Equals(TimeSpan.Zero)) {
                blobDelayTimer = gameTime.TotalGameTime + TimeSpan.FromSeconds(1.0);
            }

            /* If the timer is over, reset the timer and return that it is time
             * for the next blob to generate. */
            if (gameTime.TotalGameTime >= blobDelayTimer) {
                isReady = true;
                blobDelayTimer = TimeSpan.Zero;
            }

            return isReady;
        }

        #endregion

        #region Gray Blob Methods

        /// <summary>
        /// Adds the value to the current GrayBlob count.
        /// </summary>
        /// <param name="amount"></param>
        public void AddGrayBlobs(int amount) {
            grayBlobBuffer += amount;
        }

        /// <summary>
        /// Adds all the current Gray Blobs to the PlayingArea.
        /// </summary>
        private void AddGrayBlobsToGrid() {
            int y = INITIAL_BLOB_Y;

            while (grayBlobBuffer > 0) {
                if (grayBlobBuffer >= blobs.Width) {
                    for (int x = 0; x < blobs.Width; x++) {
                        Blob blob = new Blob(Blob.Gray, new Vector2(x, y));
                        blobs.AddToPlayingArea(blob);
                    }

                    grayBlobBuffer -= blobs.Width;
                    y--;
                }
                else {
                    AWHatPicker rng = new AWHatPicker(blobs.Width);

                    while (grayBlobBuffer > 0) {
                        int x = rng.Pick();
                        Blob blob = new Blob(Blob.Gray, new Vector2(x, y));

                        blobs.AddToPlayingArea(blob);
                        grayBlobBuffer--;
                    }
                }
            }
        }

        #endregion

        #region Game Over Method

        /// <summary>
        /// Determines when the PlayingArea can no longer add any Blobs to the
        /// area. 
        /// </summary>
        /// <returns></returns>
        public bool IsGameOver() {
            GridResult result = blobs.GetVacancyFor(INITIAL_BLOB_X, INITIAL_BLOB_Y + 1);
            bool isOccup = (result== GridResult.OCCUPIED);

            /* The game is over (true), when both are true
             *      A. The blobs currently resolved  
             *      B. The initial spot is occupied */
            return blobs.IsResolved && isOccup;
        }

        #endregion

        #region Scoring System

        /// <summary>
        /// Calculates the score based off the current information contained 
        /// on the score stack. 
        /// </summary>
        /// <returns></returns>
        private int CalculateScoreStack() {
            int addedScore = 0;

            if (scoreStack.Count > 0) {
                while (scoreStack.Count > 0) {
                    int count = scoreStack.Pop();

                    addedScore += (int)Math.Pow(count, scoreStack.Count);
                }
            }

            return addedScore;
        }

        #endregion

        #region Draw Method

        /// <summary>
        /// Draws the PlayingArea. It includes drawing for BlobArray and the 
        /// two BlobCombos.
        /// </summary>
        /// <param name="spriteBatch"></param>
        /// <param name="content"></param>
        public void Draw(SpriteBatch spriteBatch, ContentManager content) {
            SpriteFont gameFont = content.Load<SpriteFont>("gamefont");
            String text = grayBlobBuffer.ToString();
            Vector2 textPos = PlayingArea.GraphicOffset - (gameFont.MeasureString(text) * 0.5f);

            blobs.Draw(spriteBatch, content);
            spriteBatch.DrawString(gameFont, text, textPos, Color.Black);

            if (activeCombo != null) {
                activeCombo.Draw(spriteBatch, content);
            }

            if (nextCombo != null) {
                DrawNextCombo(spriteBatch, content);
            }
        }

        /// <summary>
        /// A specialized Draw method needed to nicely draw the next combo.
        /// </summary>
        /// <param name="spriteBatch"></param>
        /// <param name="content"></param>
        private void DrawNextCombo(SpriteBatch spriteBatch, ContentManager content) {
            Vector2 offset = new Vector2(8, 1);
            Blob lead = nextCombo.LeadBlob;
            Blob follow = nextCombo.FollowBlob;

            lead.Position += offset;
            follow.Position += offset;

            nextCombo.LeadBlob.Draw(spriteBatch, content);
            nextCombo.FollowBlob.Draw(spriteBatch, content);

            lead.Position -= offset;
            follow.Position -= offset;
        }

        #endregion

        #region Network Sync
        
        /// <summary>
        /// Updates the PlayingArea based on the data received from the other
        /// player.
        /// </summary>
        /// <param name="data"></param>
        public void ImportData(byte[] data) {
            blobs.ImportData(data);
        }

        /// <summary>
        /// Generates a byte area for the other player to receive, that will 
        /// represent any changes to the PlayingArea.
        /// </summary>
        /// <returns></returns>
        public byte[] ExportData() {
            return blobs.ExportData();
        }

        #endregion
    }
}